#!/usr/bin/env python3

import os
import base64

import bcrypt
import tornado.web

from datetime import datetime as dt
from concurrent.futures import ThreadPoolExecutor

from bson import ObjectId
from tornado import gen
from tornado.escape import utf8

from modules import CommentModule
from constants import ENTRIES_PER_PAGE



class BaseHandler(tornado.web.RequestHandler):
    @property
    def db(self):
        return self.application.db

    def find_by_id(self, coll, _id):
        _id = _id.decode() if isinstance(_id, bytes) else _id
        return self.db[coll].find_one(ObjectId(_id))

    def get_entries_block(self, page, block_size, **kws):
        '''Get a set of entries by size `block_size` of the `page`-th page'''
        cursor = self.db['entries'].find(kws)
        cursor.sort('_id')
        cursor.skip(page * block_size)
        cursor.limit(block_size)
        # return the pages of block and the total-pages-number in float
        return [entry for entry in cursor], cursor.count() / block_size

    def get_current_user(self):
        user_id = self.get_secure_cookie('user_id')
        return self.find_by_id('authors', user_id) if user_id else None


class HomeHandler(BaseHandler):
    def get(self):
        page = int(self.get_argument('page', 1))
        entries, last = self.get_entries_block(page - 1, ENTRIES_PER_PAGE)
        self.render('home.html', entries=entries, page=page, last=last)



# Author Handlers

class AuthHandler(BaseHandler):
    pool = ThreadPoolExecutor(5)

    def hashpw(password, salt=None):
        '''salt can be `bcrypt.gensalt()` or `hashpw` that already exists'''
        salt = utf8(salt) if salt else bcrypt.gensalt()
        return AuthHandler.pool.submit(bcrypt.hashpw, utf8(password), salt)


class AuthManageHandler(AuthHandler):
    @tornado.web.authenticated
    def get(self, _id):
        page = int(self.get_argument('page', 1))
        entries, last = self.get_entries_block(page - 1, ENTRIES_PER_PAGE,
                                               author=ObjectId(_id))
        self.render('auth/manage.html', entries=entries, page=page, last=last)


class AuthCreateHandler(AuthHandler):
    def get(self):
        if self.current_user:
            self.redirect('/')
        else:
            self.render('auth/create.html', error=None)

    @gen.coroutine
    def post(self):
        if self.current_user:
            self.redirect('/auth/logout?next=/auth/create')
            return
        email = self.get_argument('email')
        if self.db['authors'].find_one({'email': email}):
            self.render('auth/create.html', error='Email has been registered')
        else:
            # But password still being transformed in plaintext, how ?
            hashpw = yield AuthHandler.hashpw(self.get_argument('password'))
            res = self.db['authors'].insert_one({
                # `created` was implicitly defined in `_id` but it was UTC
                'created': dt.now().timestamp(),
                'email': email,
                'name': self.get_argument('name'),
                'hashpw': hashpw,
            })
            self.set_secure_cookie('user_id', str(res.inserted_id))
            self.redirect(self.get_argument('next', '/'))


class AuthLoginHandler(AuthHandler):
    def get(self):
        self.render('auth/login.html', error=None)

    @gen.coroutine
    def post(self):
        author = self.db['authors'].find_one({
            'email': self.get_argument('email')
        })
        if not author:
            self.render('auth/login.html', error='Email not found')
        else:
            hashpw = yield AuthHandler.hashpw(self.get_argument('password'),
                                              author['hashpw'])
            if hashpw == author['hashpw']:
                self.set_secure_cookie('user_id', str(author['_id']))
                self.redirect(self.get_argument('next', '/'))
            else:
                self.render('auth/login.html', error='Wrong password')


class AuthLogoutHandler(AuthHandler):
    def get(self):
        self.clear_cookie('user_id')
        self.redirect(self.get_argument('next', '/'))


class AuthForgetPassHandler(AuthHandler):
    def get(self):
        self.render('auth/forget_pass.html')

    @gen.coroutine
    def post(self):
        email = self.get_argument('email')
        deadline = dt.now().timestamp() + 1800
        key = os.urandom(32)  # send
        proof = self.db['proofs'].find_one({'email': email})
        if proof:
            proof.update({
                'deadline': deadline,
                'random': key,
            })
            self.db['proofs'].save(proof)
        else:
            self.db['proofs'].insert_one({
                'email': email,
                'deadline': deadline,
                'random': key,
            })
        url = ('http://localhost:8000/auth/recovery?token='
               + base64.urlsafe_b64encode(key).decode())
        self.render('auth/mail_sent.html', url=url)


class AuthRecoveryHandler(AuthHandler):
    def get(self):
        token = base64.urlsafe_b64decode(self.get_argument('token'))
        proof = self.db['proofs'].find_one({'random': token})
        if not proof:
            raise tornado.web.HTTPError(403, 'Fake Proof!')
        deadline = proof['deadline']
        now_ts = dt.now().timestamp()
        self.db['proofs'].delete_one(proof)
        if deadline < now_ts:
            raise tornado.web.HTTPError(403, 'Deadline timeout')
        else:
            self.render('auth/recovery.html', email=proof['email'])

    @gen.coroutine
    def post(self):
        email = self.get_argument('email')
        author = self.db['authors'].find_one({'email': email})
        author['hashpw'] = yield AuthHandler.hashpw(self.get_argument('new_pass'))
        author['updated'] = dt.now().timestamp()
        self.db['authors'].save(author)
        self.redirect(self.get_argument('next', '/'))


class AuthUpdatePassHandler(AuthHandler):
    @tornado.web.authenticated
    def get(self):
        self.render('auth/update_pass.html')

    @tornado.web.authenticated
    @gen.coroutine
    def post(self):
        author = self.current_user
        hashpw = yield AuthHandler.hashpw(self.get_argument('old_pass'),
                                          author['hashpw'])
        if hashpw != author['hashpw']:
            raise tornado.web.HTTPError(403, 'Incorrect old password')
        author['hashpw'] = yield AuthHandler.hashpw(self.get_argument('new_pass'))
        author['updated'] = dt.now().timestamp()
        self.db['authors'].save(author)
        self.redirect(self.get_argument('next', '/'))



# Entry Handlers

class EntryEditHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        _id = self.get_argument('_id', None)
        entry = self.find_by_id('entries', _id) if _id else None
        self.render('entry/edit.html', entry=entry)

    @tornado.web.authenticated
    @gen.coroutine
    def post(self):
        _id = self.get_argument('_id', None)
        title = self.get_argument('title')
        text = self.get_argument('text')
        if _id:
            entry = self.find_by_id('entries', _id) if _id else None
            if not entry:
                raise tornado.web.HTTPError(404)
            elif entry['author'] != self.current_user['_id']:
                raise tornado.web.HTTPError(403)
            entry.update({
                'title': title,
                'text': text,
                'updated': dt.now().timestamp()
            })
            self.db['entries'].save(entry)
        else:
            res = self.db['entries'].insert_one({
                'title': title,
                'text': text,
                'created': dt.now().timestamp(),
                'author': self.current_user['_id'],
            })
            _id = res.inserted_id
        self.redirect(self.get_argument('next', '/entry/show/%s' % _id))


class EntryDeleteHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, _id):
        entry = self.find_by_id('entries', _id)
        self.db['entries'].delete_one({'_id': ObjectId(_id)})
        self.redirect(self.get_argument('next', '/'))


class EntryShowHandler(BaseHandler):
    def get(self, _id):
        ent = self.find_by_id('entries', _id)
        auth = self.find_by_id('authors', ent['author'])
        cmts = list(self.db['comments'].find({'entry': _id}))
        self.render('entry/show.html', entry=ent, comments=cmts, author=auth)


class EntryCommentHandler(BaseHandler):
    @tornado.web.authenticated
    def post(self):
        comment = {
            'text': self.get_argument('text'),
            'author': self.get_argument('author'),
            'entry': self.get_argument('entry'),
            'created': dt.now().timestamp(),
        }
        res = self.db['comments'].insert_one(comment)
        comment.update({'_id': res.inserted_id})
        self.write(CommentModule(self).render(comment))
